"""
C++ Code Analyzer - Core analysis engine combining multiple parsing approaches
"""

import os
import re
import json
import glob
from typing import Dict, List, Optional, Set, Tuple, Any, Union
from dataclasses import dataclass, field
from pathlib import Path
from collections import defaultdict
from datetime import datetime
import tempfile
import subprocess

try:
    from clang.cindex import Index, CursorKind, CompilationDatabase
    CLANG_AVAILABLE = True
except ImportError:
    CLANG_AVAILABLE = False


@dataclass
class FunctionInfo:
    """Information about a function."""
    name: str
    namespace: str = ""
    class_name: Optional[str] = None
    return_type: str = ""
    parameters: List[str] = field(default_factory=list)
    line_count: int = 0
    source_file: str = ""
    start_line: int = 0
    end_line: int = 0
    source_code: str = ""
    is_declaration: bool = False
    is_definition: bool = True
    
    @property
    def full_name(self) -> str:
        """Get the fully qualified name of the function."""
        components = []
        if self.namespace:
            components.append(self.namespace)
        if self.class_name:
            components.append(self.class_name)
        components.append(self.name)
        return "::".join(components)


@dataclass
class CallGraph:
    """Represents function call relationships."""
    edges: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set))
    
    def add_call(self, caller: str, callee: str):
        """Add a call relationship."""
        self.edges[caller].add(callee)
    
    def get_callees(self, caller: str) -> Set[str]:
        """Get functions called by the given function."""
        return self.edges.get(caller, set())
    
    def get_callers(self, callee: str) -> Set[str]:
        """Get functions that call the given function."""
        callers = set()
        for caller, callees in self.edges.items():
            if callee in callees:
                callers.add(caller)
        return callers
    
    def find_entry_points(self) -> List[str]:
        """Find entry points (functions not called by others)."""
        all_callees = set()
        for callees in self.edges.values():
            all_callees.update(callees)
        
        entry_points = []
        for caller in self.edges.keys():
            if caller not in all_callees:
                entry_points.append(caller)
        
        return entry_points
    
    def detect_cycles(self) -> List[List[str]]:
        """Detect cycles in the call graph."""
        cycles = []
        visited = set()
        rec_stack = set()
        
        def dfs(node: str, path: List[str]):
            if node in rec_stack:
                # Found a cycle
                cycle_start = path.index(node)
                cycle = path[cycle_start:] + [node]
                cycles.append(cycle)
                return
            
            if node in visited:
                return
            
            visited.add(node)
            rec_stack.add(node)
            
            for neighbor in self.edges.get(node, []):
                dfs(neighbor, path + [neighbor])
            
            rec_stack.remove(node)
        
        for node in self.edges.keys():
            if node not in visited:
                dfs(node, [node])
        
        return cycles


@dataclass
class AnalysisConfig:
    """Configuration for C++ code analysis."""
    paths: List[str]
    recursive: bool = True
    max_depth: int = 10
    include_standard_lib: bool = False
    include_external: bool = False
    exclude_functions: List[str] = field(default_factory=list)
    root_functions: Optional[List[str]] = None
    include_line_counts: bool = True
    detect_cycles: bool = True
    use_clang: bool = False
    compile_commands_path: Optional[str] = None
    file_patterns: List[str] = field(default_factory=lambda: ['*.cpp', '*.cxx', '*.cc', '*.c', '*.h', '*.hpp', '*.hxx'])
    include_source_code: bool = False


@dataclass
class AnalysisResult:
    """Results of C++ code analysis."""
    functions: List[FunctionInfo]
    call_graph: CallGraph
    cycles: List[List[str]] = field(default_factory=list)
    statistics: Dict[str, Any] = field(default_factory=dict)
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    config: Optional[AnalysisConfig] = None


class RegexCppParser:
    """Regex-based C++ parser for function extraction and call analysis."""
    
    def __init__(self, config: AnalysisConfig):
        self.config = config
        self.functions: Dict[str, FunctionInfo] = {}
        self.call_graph = CallGraph()
        
        # Enhanced regex patterns
        self.function_def_pattern = re.compile(
            r'(?:^|\n)\s*'
            r'(?:static\s+|inline\s+|extern\s+|virtual\s+|const\s+|public\s+|private\s+|protected\s+)*'
            r'([a-zA-Z_][a-zA-Z0-9_]*(?:\s*\*+\s*|\s*&\s*|\s*::\s*|\s+)*)'
            r'\s+'
            r'([a-zA-Z_][a-zA-Z0-9_]*)'
            r'\s*\('
            r'([^;{]*?)'
            r'\)\s*'
            r'(?:\s*const\s*)?'
            r'(?:\s*override\s*)?'
            r'(?:\s*final\s*)?'
            r'(?:\s*noexcept\s*)?'
            r'\s*\{',
            re.MULTILINE | re.DOTALL
        )
        
        self.function_call_pattern = re.compile(
            r'(?<![a-zA-Z0-9_])'
            r'([a-zA-Z_][a-zA-Z0-9_:]*)'
            r'\s*\('
            r'([^;{}]*?)'
            r'\)',
            re.DOTALL
        )
        
        # Standard library functions to exclude
        self.standard_functions = {
            'printf', 'scanf', 'malloc', 'free', 'strcpy', 'strlen', 'strcmp',
            'memcpy', 'memset', 'new', 'delete', 'sizeof', 'exit', 'abort',
            'min', 'max', 'sqrt', 'pow', 'sin', 'cos', 'log', 'exp',
            'push_back', 'size', 'empty', 'begin', 'end', 'insert', 'erase'
        }
    
    def parse_files(self, file_paths: List[str]) -> Tuple[Dict[str, FunctionInfo], CallGraph]:
        """Parse multiple C++ files."""
        for file_path in file_paths:
            try:
                self._parse_file(file_path)
            except Exception as e:
                print(f"Warning: Error parsing {file_path}: {e}")
        
        return self.functions, self.call_graph
    
    def _parse_file(self, file_path: str):
        """Parse a single C++ file."""
        try:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
        except Exception:
            return
        
        # Remove comments
        content = self._remove_comments(content)
        
        # Extract functions
        self._extract_functions(content, file_path)
        
        # Build call graph
        self._build_call_graph(content)
    
    def _remove_comments(self, content: str) -> str:
        """Remove C/C++ comments from code."""
        # Remove single-line comments
        content = re.sub(r'//.*$', '', content, flags=re.MULTILINE)
        # Remove multi-line comments
        content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
        return content
    
    def _extract_functions(self, content: str, file_path: str):
        """Extract function definitions from content."""
        matches = self.function_def_pattern.finditer(content)
        
        for match in matches:
            return_type = match.group(1).strip()
            name = match.group(2).strip()
            params_str = match.group(3).strip()
            
            # Skip control structures
            if return_type.lower() in ['if', 'while', 'for', 'switch', 'else', 'do', 'catch', 'try']:
                continue
            if name.lower() in ['if', 'while', 'for', 'switch', 'else', 'do', 'catch', 'try']:
                continue
            
            # Parse parameters
            parameters = []
            if params_str:
                param_list = [p.strip() for p in params_str.split(',') if p.strip()]
                parameters = [self._clean_parameter(p) for p in param_list]
            
            # Find function body and calculate line count
            start_pos = match.end()
            function_end = self._find_function_end(content, start_pos)
            
            if function_end != -1:
                body = content[start_pos:function_end]
                line_count = body.count('\n')
                start_line = content[:match.start()].count('\n') + 1
                end_line = start_line + line_count
                
                # Create function info
                func_info = FunctionInfo(
                    name=name,
                    return_type=return_type,
                    parameters=parameters,
                    line_count=line_count,
                    source_file=file_path,
                    start_line=start_line,
                    end_line=end_line,
                    source_code=body  # Always store for call graph analysis
                )
                
                self.functions[func_info.full_name] = func_info
    
    def _clean_parameter(self, param: str) -> str:
        """Clean parameter string to extract type and name."""
        # Remove default values
        param = re.sub(r'=.*$', '', param).strip()
        # Simplify complex types
        param = re.sub(r'\s+', ' ', param)
        return param
    
    def _find_function_end(self, content: str, start_pos: int) -> int:
        """Find the end of a function by matching braces."""
        brace_count = 1
        i = start_pos
        
        while i < len(content) and brace_count > 0:
            char = content[i]
            if char == '{':
                brace_count += 1
            elif char == '}':
                brace_count -= 1
            i += 1
        
        return i if brace_count == 0 else -1
    
    def _build_call_graph(self, content: str):
        """Build call graph from function calls in the content."""
        for func_name, func_info in self.functions.items():
            # Find calls within this function's body
            if func_info.source_code:
                self._find_calls_in_function(func_info.source_code, func_name)
    
    def _find_calls_in_function(self, function_body: str, caller_name: str):
        """Find function calls within a function body."""
        matches = self.function_call_pattern.finditer(function_body)
        
        for match in matches:
            called_name = match.group(1)
            
            # Skip if it's the same function (recursion handled separately)
            if called_name == caller_name.split('::')[-1]:
                continue
            
            # Skip standard library functions unless requested
            if not self.config.include_standard_lib and called_name in self.standard_functions:
                continue
            
            # Skip excluded functions
            if called_name in self.config.exclude_functions:
                continue
            
            # Check if this function exists in our parsed functions
            # Try exact match first, then partial match
            found_function = None
            for full_name in self.functions.keys():
                if full_name == called_name or full_name.endswith('::' + called_name):
                    found_function = full_name
                    break
            
            if found_function:
                self.call_graph.add_call(caller_name, found_function)


class ClangCppParser:
    """Clang-based C++ parser for more accurate analysis."""
    
    def __init__(self, config: AnalysisConfig):
        self.config = config
        self.functions: Dict[str, FunctionInfo] = {}
        self.call_graph = CallGraph()
    
    def parse_files(self, file_paths: List[str]) -> Tuple[Dict[str, FunctionInfo], CallGraph]:
        """Parse files using Clang AST."""
        if not CLANG_AVAILABLE:
            raise RuntimeError("Clang Python bindings not available")
        
        if self.config.compile_commands_path:
            return self._parse_with_compile_commands()
        else:
            return self._parse_files_directly(file_paths)
    
    def _parse_with_compile_commands(self) -> Tuple[Dict[str, FunctionInfo], CallGraph]:
        """Parse using compile_commands.json."""
        try:
            with open(self.config.compile_commands_path, 'r') as f:
                compile_commands = json.load(f)
        except Exception as e:
            raise RuntimeError(f"Error reading compile commands: {e}")
        
        index = Index.create()
        
        for cmd in compile_commands:
            file_path = cmd.get('file', '')
            if not file_path:
                continue
            
            # Extract compilation arguments
            args = []
            if 'arguments' in cmd:
                args = [arg for arg in cmd['arguments'] if self._is_useful_arg(arg)]
            elif 'command' in cmd:
                command_parts = cmd['command'].split()
                args = [arg for arg in command_parts if self._is_useful_arg(arg)]
            
            try:
                tu = index.parse(file_path, args)
                if tu:
                    self._process_translation_unit(tu)
            except Exception as e:
                print(f"Warning: Error parsing {file_path} with Clang: {e}")
        
        return self.functions, self.call_graph
    
    def _parse_files_directly(self, file_paths: List[str]) -> Tuple[Dict[str, FunctionInfo], CallGraph]:
        """Parse files directly without compile commands."""
        index = Index.create()
        
        for file_path in file_paths:
            try:
                tu = index.parse(file_path, ['-std=c++17'])
                if tu:
                    self._process_translation_unit(tu)
            except Exception as e:
                print(f"Warning: Error parsing {file_path} with Clang: {e}")
        
        return self.functions, self.call_graph
    
    def _is_useful_arg(self, arg: str) -> bool:
        """Check if a compilation argument is useful for parsing."""
        useful_prefixes = ['-I', '-D', '-std=', '-isystem']
        return any(arg.startswith(prefix) for prefix in useful_prefixes)
    
    def _process_translation_unit(self, tu):
        """Process a Clang translation unit."""
        self._extract_functions_from_cursor(tu.cursor)
        self._extract_calls_from_cursor(tu.cursor)
    
    def _extract_functions_from_cursor(self, cursor, namespace='', class_name=''):
        """Extract function definitions from Clang cursor."""
        if cursor.kind == CursorKind.FUNCTION_DECL:
            self._process_function_decl(cursor, namespace, class_name)
        elif cursor.kind == CursorKind.CXX_METHOD:
            self._process_function_decl(cursor, namespace, class_name)
        elif cursor.kind == CursorKind.NAMESPACE:
            new_namespace = namespace + '::' + cursor.spelling if namespace else cursor.spelling
            for child in cursor.get_children():
                self._extract_functions_from_cursor(child, new_namespace, class_name)
        elif cursor.kind in [CursorKind.CLASS_DECL, CursorKind.STRUCT_DECL]:
            new_class = cursor.spelling
            for child in cursor.get_children():
                self._extract_functions_from_cursor(child, namespace, new_class)
        else:
            for child in cursor.get_children():
                self._extract_functions_from_cursor(child, namespace, class_name)
    
    def _process_function_decl(self, cursor, namespace='', class_name=''):
        """Process a function declaration cursor."""
        if not cursor.spelling:
            return
        
        # Get function information
        func_info = FunctionInfo(
            name=cursor.spelling,
            namespace=namespace,
            class_name=class_name,
            return_type=cursor.result_type.spelling if cursor.result_type else '',
            source_file=str(cursor.location.file) if cursor.location.file else '',
            start_line=cursor.location.line,
            is_declaration=not cursor.is_definition(),
            is_definition=cursor.is_definition()
        )
        
        # Extract parameters
        parameters = []
        for arg in cursor.get_arguments():
            param_type = arg.type.spelling if arg.type else 'unknown'
            param_name = arg.spelling if arg.spelling else 'unnamed'
            parameters.append(f"{param_type} {param_name}")
        func_info.parameters = parameters
        
        # Calculate line count for definitions
        if cursor.is_definition():
            extent = cursor.extent
            if extent.start.line and extent.end.line:
                func_info.line_count = extent.end.line - extent.start.line + 1
                func_info.end_line = extent.end.line
        
        self.functions[func_info.full_name] = func_info
    
    def _extract_calls_from_cursor(self, cursor, current_function=''):
        """Extract function calls from cursor."""
        if cursor.kind == CursorKind.FUNCTION_DECL or cursor.kind == CursorKind.CXX_METHOD:
            if cursor.is_definition():
                func_info = self.functions.get(self._get_full_name_from_cursor(cursor))
                if func_info:
                    current_function = func_info.full_name
        
        if cursor.kind == CursorKind.CALL_EXPR:
            if cursor.referenced and current_function:
                called_func = self._get_full_name_from_cursor(cursor.referenced)
                if called_func in self.functions:
                    self.call_graph.add_call(current_function, called_func)
        
        for child in cursor.get_children():
            self._extract_calls_from_cursor(child, current_function)
    
    def _get_full_name_from_cursor(self, cursor):
        """Get fully qualified name from cursor."""
        parts = []
        current = cursor
        
        while current and current.kind != CursorKind.TRANSLATION_UNIT:
            if current.spelling:
                parts.append(current.spelling)
            current = current.semantic_parent
        
        return '::'.join(reversed(parts))


class CppCodeAnalyzer:
    """Main analyzer that orchestrates different parsing approaches."""
    
    def __init__(self, config: AnalysisConfig):
        self.config = config
    
    def analyze(self) -> AnalysisResult:
        """Perform comprehensive C++ code analysis."""
        # Collect files to analyze
        file_paths = self._collect_files()
        
        if not file_paths:
            return AnalysisResult(functions=[], call_graph=CallGraph())
        
        # Choose parser based on configuration
        if self.config.use_clang and CLANG_AVAILABLE:
            parser = ClangCppParser(self.config)
        else:
            parser = RegexCppParser(self.config)
        
        # Parse files
        functions_dict, call_graph = parser.parse_files(file_paths)
        
        # Convert to list for result and optionally clear source code
        functions = list(functions_dict.values())
        if not self.config.include_source_code:
            for func in functions:
                func.source_code = ""
        
        # Filter functions if root functions specified
        if self.config.root_functions:
            functions, call_graph = self._filter_by_root_functions(
                functions_dict, call_graph, self.config.root_functions
            )
        
        # Detect cycles if requested
        cycles = []
        if self.config.detect_cycles:
            cycles = call_graph.detect_cycles()
        
        # Generate statistics
        statistics = self._generate_statistics(functions, call_graph)
        
        return AnalysisResult(
            functions=functions,
            call_graph=call_graph,
            cycles=cycles,
            statistics=statistics,
            config=self.config
        )
    
    def _collect_files(self) -> List[str]:
        """Collect all C++ files to analyze."""
        all_files = []
        
        for path in self.config.paths:
            if os.path.isfile(path):
                if self._is_cpp_file(path):
                    all_files.append(path)
            elif os.path.isdir(path):
                files = self._collect_files_from_directory(path)
                all_files.extend(files)
        
        return list(set(all_files))  # Remove duplicates
    
    def _collect_files_from_directory(self, directory: str) -> List[str]:
        """Collect C++ files from a directory."""
        files = []
        
        if self.config.recursive:
            for pattern in self.config.file_patterns:
                pattern_path = os.path.join(directory, '**', pattern)
                files.extend(glob.glob(pattern_path, recursive=True))
        else:
            for pattern in self.config.file_patterns:
                pattern_path = os.path.join(directory, pattern)
                files.extend(glob.glob(pattern_path))
        
        return [f for f in files if self._is_cpp_file(f)]
    
    def _is_cpp_file(self, file_path: str) -> bool:
        """Check if a file is a C++ source file."""
        extensions = {'.c', '.cpp', '.cxx', '.cc', '.h', '.hpp', '.hxx'}
        return any(file_path.lower().endswith(ext) for ext in extensions)
    
    def _filter_by_root_functions(
        self, 
        functions_dict: Dict[str, FunctionInfo], 
        call_graph: CallGraph, 
        root_functions: List[str]
    ) -> Tuple[List[FunctionInfo], CallGraph]:
        """Filter analysis to only include functions reachable from root functions."""
        reachable = set()
        
        # Find all functions reachable from root functions
        for root in root_functions:
            if root in functions_dict:
                reachable.update(self._get_reachable_functions(call_graph, root))
        
        # Filter functions and call graph
        filtered_functions = [f for f in functions_dict.values() if f.full_name in reachable]
        filtered_call_graph = CallGraph()
        
        for caller, callees in call_graph.edges.items():
            if caller in reachable:
                for callee in callees:
                    if callee in reachable:
                        filtered_call_graph.add_call(caller, callee)
        
        return filtered_functions, filtered_call_graph
    
    def _get_reachable_functions(self, call_graph: CallGraph, start: str, visited: Optional[Set[str]] = None) -> Set[str]:
        """Get all functions reachable from a starting function."""
        if visited is None:
            visited = set()
        
        if start in visited:
            return visited
        
        visited.add(start)
        
        for callee in call_graph.get_callees(start):
            self._get_reachable_functions(call_graph, callee, visited)
        
        return visited
    
    def _generate_statistics(self, functions: List[FunctionInfo], call_graph: CallGraph) -> Dict[str, Any]:
        """Generate statistics about the analysis."""
        stats = {
            'total_functions': len(functions),
            'total_calls': sum(len(callees) for callees in call_graph.edges.values()),
            'average_function_length': 0,
            'largest_function': None,
            'most_called_function': None,
            'namespaces': set(),
            'classes': set(),
            'source_files': set()
        }
        
        if functions:
            # Calculate averages
            line_counts = [f.line_count for f in functions if f.line_count > 0]
            if line_counts:
                stats['average_function_length'] = sum(line_counts) / len(line_counts)
                largest_func = max(functions, key=lambda f: f.line_count)
                stats['largest_function'] = {
                    'name': largest_func.full_name,
                    'line_count': largest_func.line_count
                }
            
            # Collect namespaces, classes, and files
            for func in functions:
                if func.namespace:
                    stats['namespaces'].add(func.namespace)
                if func.class_name:
                    stats['classes'].add(func.class_name)
                if func.source_file:
                    stats['source_files'].add(func.source_file)
            
            # Convert sets to counts
            stats['namespaces'] = len(stats['namespaces'])
            stats['classes'] = len(stats['classes'])
            stats['source_files'] = len(stats['source_files'])
            
            # Find most called function
            call_counts = defaultdict(int)
            for callees in call_graph.edges.values():
                for callee in callees:
                    call_counts[callee] += 1
            
            if call_counts:
                most_called = max(call_counts.items(), key=lambda x: x[1])
                stats['most_called_function'] = {
                    'name': most_called[0],
                    'call_count': most_called[1]
                }
        
        return stats
